// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <tuple>

#include "base/bind.h"
#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/blob_handle.h"
#include "content/public/browser/notification_observer.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/site_isolation_policy.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/common/extension_urls.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/self_owned_associated_receiver.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "storage/browser/blob/blob_registry_impl.h"
#include "third_party/blink/public/common/blob/blob_utils.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "third_party/blink/public/mojom/blob/blob_url_store.mojom-test-utils.h"
#include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"

// The goal of these tests is to "simulate" exploited renderer processes, which
// can send arbitrary IPC messages and confuse browser process internal state,
// leading to security bugs. We are trying to verify that the browser doesn't
// perform any dangerous operations in such cases.
// This is similar to the security_exploit_browsertest.cc tests, but also
// includes chrome/ layer concepts such as extensions.
class ChromeSecurityExploitBrowserTest
    : public extensions::ExtensionBrowserTest {
 public:
  ChromeSecurityExploitBrowserTest() {}

  ChromeSecurityExploitBrowserTest(const ChromeSecurityExploitBrowserTest&) =
      delete;
  ChromeSecurityExploitBrowserTest& operator=(
      const ChromeSecurityExploitBrowserTest&) = delete;

  ~ChromeSecurityExploitBrowserTest() override {}

  void SetUpOnMainThread() override {
    extensions::ExtensionBrowserTest::SetUpOnMainThread();

    ASSERT_TRUE(embedded_test_server()->Start());
    host_resolver()->AddRule("*", "127.0.0.1");

    extension_ = LoadExtension(test_data_dir_.AppendASCII("simple_with_icon"));
  }

  const extensions::Extension* extension() { return extension_; }

  std::unique_ptr<content::BlobHandle> CreateMemoryBackedBlob(
      const std::string& contents,
      const std::string& content_type) {
    std::unique_ptr<content::BlobHandle> result;
    base::RunLoop loop;
    profile()->CreateMemoryBackedBlob(
        base::as_bytes(base::make_span(contents)), content_type,
        base::BindOnce(
            [](std::unique_ptr<content::BlobHandle>* out_blob,
               base::OnceClosure done,
               std::unique_ptr<content::BlobHandle> blob) {
              *out_blob = std::move(blob);
              std::move(done).Run();
            },
            &result, loop.QuitClosure()));
    loop.Run();
    EXPECT_TRUE(result);
    return result;
  }

 private:
  raw_ptr<const extensions::Extension> extension_;
};

// Subclass of ChromeSecurityExploitBrowserTest that uses --disable-web-security
// to simulate an exploited renderer.  Note that this also disables some browser
// process checks, so it's not ideal for all exploit tests.
class ChromeWebSecurityDisabledBrowserTest
    : public ChromeSecurityExploitBrowserTest {
 public:
  ChromeWebSecurityDisabledBrowserTest() {}

  ChromeWebSecurityDisabledBrowserTest(
      const ChromeWebSecurityDisabledBrowserTest&) = delete;
  ChromeWebSecurityDisabledBrowserTest& operator=(
      const ChromeWebSecurityDisabledBrowserTest&) = delete;

  ~ChromeWebSecurityDisabledBrowserTest() override {}

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ChromeSecurityExploitBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kDisableWebSecurity);
  }
};

// TODO(nasko): This test as written is incompatible with Site Isolation
// restrictions, which disallow the cross-origin pushState call.
// Find a different way to implement issuing the illegal request or just
// delete the test if we have coverage elsewhere. See https://crbug.com/929161.
IN_PROC_BROWSER_TEST_F(ChromeWebSecurityDisabledBrowserTest,
                       DISABLED_ChromeExtensionResources) {
  // Load a page that requests a chrome-extension:// image through XHR. We
  // expect this load to fail, as it is an illegal request.
  GURL foo = embedded_test_server()->GetURL("foo.com",
                                            "/chrome_extension_resource.html");

  content::DOMMessageQueue msg_queue;

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), foo));

  std::string status;
  std::string expected_status("0");
  EXPECT_TRUE(msg_queue.WaitForMessage(&status));
  EXPECT_STREQ(status.c_str(), expected_status.c_str());
}

// Tests that a normal web process cannot send a commit for a Chrome Web Store
// URL.  See https://crbug.com/172119.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CommitWebStoreURLInWebProcess) {
  GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html");

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  content::RenderFrameHost* rfh = web_contents->GetMainFrame();

  // This IPC should result in a kill because the Chrome Web Store is not
  // allowed to commit in |rfh->GetProcess()|.
  base::HistogramTester histograms;
  content::RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(),
      content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // Modify an IPC for a commit of a blank URL, which would otherwise be allowed
  // to commit in any process.
  GURL blank_url = GURL(url::kAboutBlankURL);
  GURL webstore_url = extension_urls::GetWebstoreLaunchURL();
  content::PwnCommitIPC(web_contents, blank_url, webstore_url,
                        url::Origin::Create(GURL(webstore_url)));
  web_contents->GetController().LoadURL(
      blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string());

  // If the process is killed in CanCommitURL, this test passes.
  crash_observer.Wait();
  histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1);
}

// Tests that a non-extension process cannot send a commit of a blank URL with
// an extension origin.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CommitExtensionOriginInWebProcess) {
  GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html");

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  content::RenderFrameHost* rfh = web_contents->GetMainFrame();

  // This IPC should result in a kill because |ext_origin| is not allowed to
  // commit in |rfh->GetProcess()|.
  base::HistogramTester histograms;
  content::RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(),
      content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // Modify an IPC for a commit of a blank URL, which would otherwise be allowed
  // to commit in any process.
  GURL blank_url = GURL(url::kAboutBlankURL);
  std::string ext_origin = "chrome-extension://" + extension()->id();
  content::PwnCommitIPC(web_contents, blank_url, blank_url,
                        url::Origin::Create(GURL(ext_origin)));
  web_contents->GetController().LoadURL(
      blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string());

  // If the process is killed in CanCommitOrigin, this test passes.
  crash_observer.Wait();
  histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 114,
                                1);
}

// Tests that a non-extension process cannot send a commit of an extension URL.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CommitExtensionURLInWebProcess) {
  GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html");

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  content::RenderFrameHost* rfh = web_contents->GetMainFrame();

  // This IPC should result in a kill because extension URLs are not allowed to
  // commit in |rfh->GetProcess()|.
  base::HistogramTester histograms;
  content::RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(),
      content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // Modify an IPC for a commit of a blank URL, which would otherwise be allowed
  // to commit in any process.
  GURL blank_url = GURL(url::kAboutBlankURL);
  std::string ext_origin = "chrome-extension://" + extension()->id();
  content::PwnCommitIPC(web_contents, blank_url, GURL(ext_origin),
                        url::Origin::Create(GURL(ext_origin)));
  web_contents->GetController().LoadURL(
      blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string());

  // If the process is killed in CanCommitURL, this test passes.
  crash_observer.Wait();
  histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1);
}

// Tests that a non-extension process cannot send a commit of an extension
// filesystem URL.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CommitExtensionFilesystemURLInWebProcess) {
  GURL foo = embedded_test_server()->GetURL("foo.com", "/title1.html");

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  content::RenderFrameHost* rfh = web_contents->GetMainFrame();

  // This IPC should result in a kill because extension filesystem URLs are not
  // allowed to commit in |rfh->GetProcess()|.
  base::HistogramTester histograms;
  content::RenderProcessHostWatcher crash_observer(
      rfh->GetProcess(),
      content::RenderProcessHostWatcher::WATCH_FOR_PROCESS_EXIT);

  // Modify an IPC for a commit of a blank URL, which would otherwise be allowed
  // to commit in any process.
  GURL blank_url = GURL(url::kAboutBlankURL);
  std::string ext_origin = "chrome-extension://" + extension()->id();
  content::PwnCommitIPC(web_contents, blank_url,
                        GURL("filesystem:" + ext_origin + "/foo"),
                        url::Origin::Create(GURL(ext_origin)));
  web_contents->GetController().LoadURL(
      blank_url, content::Referrer(), ui::PAGE_TRANSITION_LINK, std::string());

  // If the process is killed in CanCommitURL, this test passes.
  crash_observer.Wait();
  histograms.ExpectUniqueSample("Stability.BadMessageTerminated.Content", 1, 1);
}

// chrome://xyz should not be able to create a "filesystem:chrome://abc"
// resource.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CreateFilesystemURLInOtherChromeUIOrigin) {
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("chrome://version")));

  content::RenderFrameHost* rfh =
      browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  // Block the renderer on operation that never completes, to shield it from
  // receiving unexpected browser->renderer IPCs that might CHECK.
  rfh->ExecuteJavaScriptWithUserGestureForTests(
      u"var r = new XMLHttpRequest();"
      u"r.open('GET', '/slow?99999', false);"
      u"r.send(null);"
      u"while (1);");

  std::string payload = "<p>Hello world!</p>";
  std::string payload_type = "text/html";

  // Target an extension.
  std::string target_origin = "chrome://downloads";

  // Set up a blob ID and populate it with the attacker-controlled payload. This
  // is just using the blob APIs directly since creating arbitrary blobs is not
  // what is prohibited; this data is not in any origin.
  std::unique_ptr<content::BlobHandle> blob =
      CreateMemoryBackedBlob(payload, payload_type);
  std::string blob_id = blob->GetUUID();

  // Note: a well-behaved renderer would always send the following message here,
  // but it's actually not necessary for the original attack to succeed, so we
  // omit it. As a result there are some log warnings from the quota observer.
  //
  // IPC::IpcSecurityTestUtil::PwnMessageReceived(
  //     rfh->GetProcess()->GetChannel(),
  //     FileSystemHostMsg_OpenFileSystem(22, GURL(target_origin),
  //                                      storage::kFileSystemTypeTemporary));

  GURL target_url =
      GURL("filesystem:" + target_origin + "/temporary/exploit.html");

  content::PwnMessageHelper::FileSystemCreate(
      rfh->GetProcess(), 23, target_url, false, false, false,
      blink::StorageKey(url::Origin::Create(target_url)));

  // Write the blob into the file. If successful, this places an
  // attacker-controlled value in a resource on the extension origin.
  content::PwnMessageHelper::FileSystemWrite(
      rfh->GetProcess(), 24, target_url, blob_id, 0,
      blink::StorageKey(url::Origin::Create(target_url)));

  // Now navigate to |target_url| in a new tab. It should not contain |payload|.
  ASSERT_FALSE(AddTabAtIndex(0, target_url, ui::PAGE_TRANSITION_TYPED));
  EXPECT_FALSE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetWebContentsAt(0)));
  rfh = browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  // If the attack is unsuccessful, the navigation ends up in an error
  // page.
  if (content::SiteIsolationPolicy::IsErrorPageIsolationEnabled(
          !rfh->GetParent())) {
    EXPECT_EQ(GURL(content::kUnreachableWebDataURL),
              rfh->GetSiteInstance()->GetSiteURL());
  } else {
    EXPECT_EQ(GURL(target_origin), rfh->GetSiteInstance()->GetSiteURL());
  }
  std::string body;
  std::string script = R"(
    var textContent = document.body.innerText.replace(/\n+/g, '\n');
    window.domAutomationController.send(textContent);
  )";

  EXPECT_TRUE(content::ExecuteScriptAndExtractString(rfh, script, &body));
  EXPECT_EQ(
      "Your file couldn’t be accessed\n"
      "It may have been moved, edited, or deleted.\n"
      "ERR_FILE_NOT_FOUND",
      body);
}

// Extension isolation prevents a normal renderer process from being able to
// create a "filesystem:chrome-extension://sdgkjaghsdg/temporary/" resource.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTest,
                       CreateFilesystemURLInExtensionOrigin) {
  GURL page_url =
      embedded_test_server()->GetURL("a.root-servers.net", "/title1.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), page_url));

  content::RenderFrameHost* rfh =
      browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  // Block the renderer on operation that never completes, to shield it from
  // receiving unexpected browser->renderer IPCs that might CHECK.
  rfh->ExecuteJavaScriptWithUserGestureForTests(
      u"var r = new XMLHttpRequest();"
      u"r.open('GET', '/slow?99999', false);"
      u"r.send(null);"
      u"while (1);");

  // JS code that the attacker would like to run in an extension process.
  std::string payload = "<html><body>pwned.</body></html>";
  std::string payload_type = "text/html";

  // Target an extension.
  std::string target_origin = "chrome-extension://" + extension()->id();

  // Set up a blob ID and populate it with the attacker-controlled payload. This
  // is just using the blob APIs directly since creating arbitrary blobs is not
  // what is prohibited; this data is not in any origin.
  std::unique_ptr<content::BlobHandle> blob =
      CreateMemoryBackedBlob(payload, payload_type);
  std::string blob_id = blob->GetUUID();

  // Note: a well-behaved renderer would always call Open first before calling
  // Create and Write, but it's actually not necessary for the original attack
  // to succeed, so we omit it. As a result there are some log warnings from the
  // quota observer.

  GURL target_url =
      GURL("filesystem:" + target_origin + "/temporary/exploit.html");

  content::PwnMessageHelper::FileSystemCreate(
      rfh->GetProcess(), 23, target_url, false, false, false,
      blink::StorageKey(url::Origin::Create(target_url)));

  // Write the blob into the file. If successful, this places an
  // attacker-controlled value in a resource on the extension origin.
  content::PwnMessageHelper::FileSystemWrite(
      rfh->GetProcess(), 24, target_url, blob_id, 0,
      blink::StorageKey(url::Origin::Create(target_url)));

  // Now navigate to |target_url| in a new tab. It should not contain |payload|.
  ASSERT_FALSE(AddTabAtIndex(0, target_url, ui::PAGE_TRANSITION_TYPED));
  EXPECT_FALSE(content::WaitForLoadStop(
      browser()->tab_strip_model()->GetWebContentsAt(0)));
  rfh = browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  // If the attack is unsuccessful, the navigation ends up in an error
  // page.
  if (content::SiteIsolationPolicy::IsErrorPageIsolationEnabled(
          !rfh->GetParent())) {
    EXPECT_EQ(GURL(content::kUnreachableWebDataURL),
              rfh->GetSiteInstance()->GetSiteURL());
  } else {
    EXPECT_EQ(GURL(target_origin), rfh->GetSiteInstance()->GetSiteURL());
  }
  std::string body;
  std::string script = R"(
    var textContent = document.body.innerText.replace(/\n+/g, '\n');
    window.domAutomationController.send(textContent);
  )";

  EXPECT_TRUE(content::ExecuteScriptAndExtractString(rfh, script, &body));
  EXPECT_EQ(
      "Your file couldn’t be accessed\n"
      "It may have been moved, edited, or deleted.\n"
      "ERR_FILE_NOT_FOUND",
      body);
}

namespace {

class BlobURLStoreInterceptor
    : public blink::mojom::BlobURLStoreInterceptorForTesting {
 public:
  static void Intercept(
      GURL target_url,
      mojo::SelfOwnedAssociatedReceiverRef<blink::mojom::BlobURLStore>
          receiver) {
    auto interceptor =
        base::WrapUnique(new BlobURLStoreInterceptor(target_url));
    auto* raw_interceptor = interceptor.get();
    auto impl = receiver->SwapImplForTesting(std::move(interceptor));
    raw_interceptor->url_store_ = std::move(impl);
  }

  blink::mojom::BlobURLStore* GetForwardingInterface() override {
    return url_store_.get();
  }

  void Register(
      mojo::PendingRemote<blink::mojom::Blob> blob,
      const GURL& url,
      // TODO(https://crbug.com/1224926): Remove this once experiment is over.
      const base::UnguessableToken& unsafe_agent_cluster_id,
      RegisterCallback callback) override {
    GetForwardingInterface()->Register(std::move(blob), target_url_,
                                       unsafe_agent_cluster_id,
                                       std::move(callback));
  }

 private:
  explicit BlobURLStoreInterceptor(GURL target_url) : target_url_(target_url) {}

  std::unique_ptr<blink::mojom::BlobURLStore> url_store_;
  GURL target_url_;
};

}  // namespace

class ChromeSecurityExploitBrowserTestMojoBlobURLs
    : public ChromeSecurityExploitBrowserTest {
 public:
  ChromeSecurityExploitBrowserTestMojoBlobURLs() = default;

  void TearDown() override {
    storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(nullptr);
  }
};

// Extension isolation prevents a normal renderer process from being able to
// create a "blob:chrome-extension://" resource.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTestMojoBlobURLs,
                       CreateBlobInExtensionOrigin) {
  // Target an extension.
  std::string target_origin = "chrome-extension://" + extension()->id();
  std::string blob_path = "5881f76e-10d2-410d-8c61-ef210502acfd";
  auto intercept_hook =
      base::BindRepeating(&BlobURLStoreInterceptor::Intercept,
                          GURL("blob:" + target_origin + "/" + blob_path));
  storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(),
      embedded_test_server()->GetURL("a.root-servers.net", "/title1.html")));

  content::RenderFrameHost* rfh =
      browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  content::RenderProcessHostBadMojoMessageWaiter crash_observer(
      rfh->GetProcess());

  // The renderer should always get killed, but sometimes ExecuteScript returns
  // true anyway, so just ignore the result.
  std::ignore =
      content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))");

  // If the process is killed, this test passes.
  EXPECT_EQ(
      "Received bad user message: "
      "URL with invalid origin passed to BlobURLStore::Register",
      crash_observer.Wait());
}

// Flaky. See https://crbug.com/1224293.
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_CreateBlobInOtherChromeUIOrigin \
    DISABLED_CreateBlobInOtherChromeUIOrigin
#else
#define MAYBE_CreateBlobInOtherChromeUIOrigin CreateBlobInOtherChromeUIOrigin
#endif  // BUILDFLAG(IS_CHROMEOS)
// chrome://xyz should not be able to create a "blob:chrome://abc" resource.
IN_PROC_BROWSER_TEST_F(ChromeSecurityExploitBrowserTestMojoBlobURLs,
                       MAYBE_CreateBlobInOtherChromeUIOrigin) {
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL("chrome://version")));

  // All these are attacker controlled values.
  std::string blob_type = "text/html";
  std::string blob_contents = "<p>Hello world!</p>";
  std::string blob_path = "f7dfbeb5-8e41-4c4a-8486-a52fed33c4c0";

  // Target an extension.
  std::string target_origin = "chrome://downloads";

  auto intercept_hook =
      base::BindRepeating(&BlobURLStoreInterceptor::Intercept,
                          GURL("blob:" + target_origin + "/" + blob_path));
  storage::BlobRegistryImpl::SetURLStoreCreationHookForTesting(&intercept_hook);

  content::RenderFrameHost* rfh =
      browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();

  content::RenderProcessHostBadMojoMessageWaiter crash_observer(
      rfh->GetProcess());

  // The renderer should always get killed, but sometimes ExecuteScript returns
  // true anyway, so just ignore the result.
  std::ignore =
      content::ExecuteScript(rfh, "URL.createObjectURL(new Blob(['foo']))");

  // If the process is killed, this test passes.
  EXPECT_EQ(
      "Received bad user message: "
      "URL with invalid origin passed to BlobURLStore::Register",
      crash_observer.Wait());
}
